/*
 * Licensed to Nanjing RedOrange Co. ltd (RedOrange) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * RedOrange licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package redora.impl;

import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import redora.api.Persistable;
import redora.api.PersistableSet;
import redora.exceptions.ObjectNotFoundException;

public class PersistableHashSet<E> extends AbstractSet<E> implements
        PersistableSet<E> {

    private static final long serialVersionUID = 1L;
    protected final HashMap<Long, E> map;
    /** Number of new (id == null) objects added to the set. */
    protected int newObjectCount = 0;

    /**
     * List of recently deleted relations with already existing counterparts
     * (isNew = false, id != null).
     */
    private Set<E> removedObjects;
    private boolean isRetrieved = false;

    /**
     * Constructs a new, empty set; the backing
     * <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75). <br />
     * Unchanged from HashMap implementation
     */
    public PersistableHashSet() {
        map = new HashMap<Long, E>();
    }

    /**
     * Returns an iterator over the elements in this set. The elements are
     * returned in no particular order.
     *
     * <br />
     * Unchanged from HashMap implementation
     *
     * @return an Iterator over the elements in this set
     */
    public Iterator<E> iterator() {
        return map.values().iterator();
    }

    /**
     * Returns the number of elements in this set (its cardinality).
     *
     * <br />
     * Unchanged from HashMap implementation
     *
     * @see HashMap#size()
     * @return the number of elements in this set (its cardinality)
     */
    public int size() {
        return map.size();
    }

    /**
     * Returns <tt>true</tt> if this set contains no elements. <br />
     * Unchanged from HashMap implementation
     *
     * @return <tt>true</tt> if this set contains no elements
     */
    public boolean isEmpty() {
        return map.isEmpty();
    }

    /**
     * Returns <tt>true</tt> if this set contains
     * the specified element. More
     * formally, returns <tt>true</tt> if and only if this set contains an
     * element <tt>e</tt> such that <tt>(o==null ?
     * e==null: o.equals(e))</tt>. <br />
     * Unchanged from HashMap implementation
     *
     * @param o
     *            element whose presence in this set is to be tested
     * @return <tt>true</tt> if this set contains the specified element
     */
    public boolean contains(Object o) {
        return map.containsValue(o);
    }

    /**
     * Adds the specified element to this set if it is not
     * already present. More
     * formally, adds the specified element <tt>e</tt> to
     * this set if this set
     * contains no element <tt>e2</tt> such that
     * <tt>(e==null ? e2==null : e.equals(e2))</tt>. If this set already
     * contains the element, the call leaves the set unchanged and returns
     * <tt>false</tt>.
     *
     * @param e
     *            element to be added to this set
     * @return <tt>true</tt> if this set did not already
     *    contain the specified element
     */
    public boolean add(E e) {
        Persistable p = (Persistable) e;
        if (p.getId() != null) {
            return map.put(p.getId(), e) == null;
        }

        return map.put(Long.valueOf(-++newObjectCount), e) == null;
    }

    /**
     * Convenience method: adds the objects and returns
     * exactly the same object.
     * Usage: <br />
     * PersistableParent parent = new PersistableParent();<br />
     * PersistableChild child = parent.getChildren().insert(new
     * PersistableChild());<br />
     * child.setSomething("else");<br />
     * <br />
     * service.persist(parent); <br/>
     * <br/>
     * Above example creates and persists a new parent and child.
     * So, you need only line of code to initialize a new child.
     */
    public E insert(E e) {
        add(e);
        return e;
    }

    /**
     * Removes the specified element from this set if it is present. More
     * formally, removes an element <tt>e</tt> such that
     * <tt>(o==null ? e==null : o.equals(e))</tt>, if this set contains
     * such an element. Returns <tt>true</tt> if this set contained the
     * element (or equivalently, if this set changed as a result of the
     * call). (This set will not contain the element once the call
     * returns.)
     *
     * @param o
     *            object to be removed from this set, if present
     * @return <tt>true</tt> if the set contained the specified element
     */
    @Override
    public boolean remove(Object o) {
        Long id = ((Persistable) o).getId();
        if (id != null) {
            return removeById(id);
        }
        return removeNotUsingId(o);
    }

    private boolean removeNotUsingId(Object o) {
        for (long i = -1; i >= -newObjectCount; i--) {
            if (map.containsKey(Long.valueOf(i))
                    && map.get(i) == o) {
                map.remove(Long.valueOf(i));
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean removeAll(Collection<?> arg0) {
        for (Object o : arg0) {
            if (!remove(o)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Layer method. Sets everything to the persisted state.
     * Invoke this after you have persisted all.
     */
    public void clear() {
        removedObjects = null;
        newObjectCount = 0;
        isRetrieved = true;
    }

    public E getById(Long id) {
        return map.get(id);
    }

    public boolean removeById(Long id) {
        getRemovedObjects().add(getById(id));
        return map.remove(id) != null;
    }

    /**
     * Method used by the persistence layer itself.
     * Sets the Id (key) of a new
     * child after they are persisted in the database.
     */
    public void setMapKey(E e) throws ObjectNotFoundException {
        Persistable p = (Persistable) e;
        if (getById(p.getId()) == null) {
            if (!removeNotUsingId(p)) {
                throw new ObjectNotFoundException(
                       "Can't remove it from the list because "
                        + e.getClass()
                        + " with id: " + p.getId() + " was not found.",
                        null);
            }
            add(e);
        }
    }

    public Set<E> getRemovedObjects() {
        if (removedObjects == null) {
            removedObjects = new HashSet<E>();
        }
        return removedObjects;
    }

    public void setRetrieved(boolean isRetrieved) {
        this.isRetrieved = isRetrieved;
    }

    /**
     * True when the list is retrieved from the database,
     * or when it is a new
     * collection. Used for lazy retrieval of children.
     */
    public boolean isRetrieved() {
        return isRetrieved;
    }

    public boolean isDirty() {
        if (newObjectCount > 0) {
            return true;
        }
        if (removedObjects != null && !removedObjects.isEmpty()) {
            return true;
        }
        for (E p : map.values()) {
            if (((Persistable) p).isDirty()) {
                return true;
            }
        }
        return false;
    }
}
